Warsztat.GDCompo!ProjektyMediaArtykułyQ&AForumOferty pracyPobieranie

Opisz napotkaną sytuację, a redakcja niezwłocznie znajdzie rozwiązanie!

wyślij anuluj

Własny język skryptowy, część 3

Tekst został importowany z Warsztatowych artykułów. Jego oryginalnym autorem jest Złośliwiec. Jeżeli został importowany poprawnie, usuń ten szablon!

Procedury

Czym jest procedura? Proste - jest to ciąg instrukcji z przypisaną im nazwą. Ustalamy więc, że przykładowa definicja procedury w naszym języku wyglądać będzie tak:

defproc test
{
 con_out "To jest testowa procedura."
}

Na czym zaś polega wywołanie procedury? Jest to po prostu skok do miejsca, gdzie zdefiniowane są instrukcje, wchodzące w skład danej procedury, a następnie powrót do miejsca wywołania. Nie bez powodu więc twierdziliśmy w poprzedniej części artykułu, że procedurę można łatwo zrealizować przy pomocy dwóch instrukcji goto. Takie rozwiązanie jest jednak dość uciążliwe na dłuższą metę i właśnie dlatego wprowadzimy teraz do naszego języka procedurę jako odrębny element tego języka.

Najpierw, jak zwykle, dodajemy nowe polecenia do typu wyliczeniowego ECmdType. Potrzebne nam będą dwa: definicja procedury oraz wywołanie:

enum ECmdType { CMD_UNKNOWN, CMD_ASSIGN_VALUE, CMD_RBRACKET, CMD_IF, CMD_LABEL, CMD_GOTO, CMD_DEFPROC, //nowe CMD_CALLPROC, //nowe CMD_CON_OUT };

Parsing polecenia defproc nie niesie za sobą nic specjalnie nowego:

else if(cword == "defproc") { cmd_info.Type = CMD_DEFPROC; if(!get_token()) { cout << "Blad, spodziewalem sie nazwy procedury..." << endl; return false; } cmd_info.s1 = cword; if(!get_token() || cword != "{") { cout << "Blad, spodziewalem sie znaku \'{\'..." << endl; return false; } cmd_info.nNestingLevel = ++nCurNestingLevel; }

Jeszcze mniej problemu z poleceniem call:

else if(cword == "call") { cmd_info.Type = CMD_CALLPROC; if(!get_token()) { cout << "Blad, spodziewalem sie nazwy procedury..." << endl; return false; } cmd_info.s1 = cword; }

Nieco większe wyzwania czekają nas w warstwie wykonania. Jeśli chodzi o polecenie defproc, to mamy obowiązek pominięcia wszystkich instrukcji zawartych w jego bloku, o ile nie zostały wywołane przez call. Z kolei w tym drugim poleceniu musimy znaleźć odpowiedni blok defproc i wykonać go. Robimy to oczywiście przez wywołanie execute_commands dla odpowiedniego zakresu poleceń. I tu leży pewien problem: musimy wykonać wszystkie te czynności, co i w przypadku rekurencyjnego wywołania execute_commands dla instrukcji if. Tak więc czeka nas trochę kopiowania tamtego kodu:

case CMD_CALLPROC: { int rbracket = -1, def = -1; for(int j=0; j<cmd_array.size(); ++j) //szukanie definicji { if(cmd_array[j].Type == CMD_DEFPROC && cmd_array[j].s1 == cmd_array[i].s1) { def = j; break; } } if(def < 0) { bError = true; cout << "Blad wykonania skryptu - nie znaleziono procedury " << cmd_array[i].s1 << endl; return true; } for(int j=def+1; j<cmd_array.size(); ++j) //szukanie prawej klamry { if(cmd_array[j].Type == CMD_RBRACKET && cmd_array[j].nNestingLevel == cmd_array[def].nNestingLevel) { rbracket = j; break; } } if(rbracket < 0) { bError = true; cout << "Blad wykonania skryptu - niedomkniety blok defproc" << endl; return true; } //wykonanie procedury ++nNestingLevel; execute_commands(cmd_array, def+1, rbracket); if(bError) return true; if(bUnwind) { if(nNestingLevel > 0) { --nNestingLevel; return false; } else { bUnwind = false; i = nNewPos; goto InsideLoop; } } // if(bUnwind) } break;

Myślę, że powyższy kod, mimo iż trochę przydługi, nie jest niejasny. Najpierw szukamy definicji procedury o nazwie takiej samej, jak parametr polecenia call. Następnie poszukujemy prawego nawiasu klamrowego (musi on mieć poziom zagnieżdżenia 1 - nasz język nie zezwala na zagnieżdżanie procedur). Nawias ten oznacza oczywiście zakończenie ciała procedury. Gdy już mamy obie te rzeczy, wywołujemy execute_commands dla przedziału: od początku definicji do prawej klamry. Jest to właśnie ów fragment, niemal identyczny z tym, który napisaliśmy dla instrukcji goto.

Pozostaje do zrobienia jedna, bardzo ważna rzecz. Otóż musimy jakoś zapewnić pomijanie definicji procedury w momencie, gdy nie jest ona wywołana (tj. licznik i po prostu doszedł właśnie do tego miejsca w tablicy cmd_array, gdzie znajduje się definicja procedury). Jednym z najprostszych sposobów jest... obsłużenie polecenia defproc! Do tej pory nie robiliśmy tego, gdyż polecenie to było umieszczane w tablicy poleceń tylko po to, by nasz interpreter wiedział, gdzie zaczyna się procedura. Ponieważ jednak wywołujemy execute_commands z parametrem def+1 (czyli pomijamy instrukcję def_proc), to możemy spokojnie obsłużyć to polecenie i wstawić tam następujące czynności:

  • znalezienie prawej klamry (końca procedury)
  • przeskoczenie do znalezionego punktu

A oto jak to zrobimy:

case CMD_DEFPROC: { int rbracket = -1; for(int j=i+1; j<cmd_array.size(); ++j) { if(cmd_array[j].Type == CMD_RBRACKET && cmd_array[j].nNestingLevel == cmd_array[i].nNestingLevel) { rbracket = j; break; } } if(rbracket < 0) { bError = true; cout << "Blad wykonania skryptu - niedomkniety blok defproc" << endl; return true; } i = rbracket; } break;

Poszukiwanie prawej klamry żywcem skopiowaliśmy z naszego dotychczasowego kodu; zmieniliśmy tylko instrukcję inicjalizacji w pętli for. Reszta jest identyczna. Po znalezieniu klamry przesuwamy nad nią licznik i i kontynuujemy główną pętlę z tym nowym licznikiem, czyli przechodzimy do instrukcji następującej po ciele procedury. Pora przetestować nowy element naszego języka:

var z 10

// procedurka :-)
defproc testpr
{
 con_out "Test procedurki"
 if z != 10
 {
  con_out "Bla."
 }
 con_out "Poza warunkiem"
}
// wywołaj procedurkę
call testpr
call testpr

Warto zauważyć, że w przeciwieństwie do języka C++, u nas możliwe jest umieszczenie wywołania funkcji jeszcze przed jej definicją:

call test
con_out "Teraz nastapi definicja"
defproc test
{
 con_out "Oto procedura"
}

Pętle

Kolejnym niezbędnym elementem każdego języka programowania, którego jeszcze nie mamy, są pętle. Zrealizujemy sobie teraz ten rodzaj pętli, który (jak się powszechnie przyjęło) nazywa się pętlą for. Najlepszą chyba powstałą implementacją takiej pętli jest ta z języka C. Niestety, potrzebny jest do niej interpreter, który potrafi obliczać bardziej złożone wyrażenia, a jego ze względu na rozmiar artykułu pisać nie będziemy :-). Dlatego zadowolimy się najprostszą możliwą postacią pętli for, która będzie się prezentowała jakoś tak:

for i = 1 to 10
{
 //instrukcje
}

Tradycyjnie już, zaczynamy od dodania odpowiedniej stałej dla nowego polecenia:

enum ECmdType { CMD_UNKNOWN, CMD_ASSIGN_VALUE, CMD_RBRACKET, CMD_IF, CMD_LABEL, CMD_GOTO, CMD_DEFPROC, CMD_CALLPROC, CMD_FOR //nowe };

Następnie - też tradycyjnie - modyfikujemy parser. Pętla for nie jest zwykłym poleceniem - łączy się ona ze zmienną, pełniącą rolę licznika. W języku C (i wielu innych) programista ma tu do wyboru: albo użyje zadeklarowanej już wcześniej zmiennej, albo zadeklaruje nową w bloku kontrolnym pętli. My nie chcemy sobie zanadto komplikować sytuacji, więc w naszym języku skryptowym damy tylko tę drugą możliwość, przy czym deklaracja zmiennej-licznika będzie niejawna (podobnie jak w języku Basic). Oczywiście nie możemy przechowywać lokalnej zmiennej w naszym wektorze Variables, gdyż nie chcemy, aby był do niej dostęp z zewnątrz pętli ani żeby powodowała ona ewentualne konflikty nazw. Dlatego stworzymy dla zmiennych lokalnych (liczników pętli) oddzielną tablicę. A ponieważ nasze pętle będzie się dało zagnieżdżać, zmienne te dobrze by było przechowywać na stosie. Dołączamy więc niezbędny nagłówek:

#include <stack>

...i deklarujemy nasz stos dla lokalnych zmiennych:

stack<VARIABLE> LocalVariables;

Na ten stos będziemy wrzucać licznik pętli przed pierwszą jej iteracją, zaś zdejmować go po wykonaniu ostatniej iteracji. W funkcji parse nie ruszamy stosu w ogóle; nie jest to potrzebne. Wystarczy, że zapamiętamy sobie nazwę zmiennej-licznika:

else if(cword == "for") { cmd_info.Type = CMD_FOR; if(!get_token()) { cout << "Blad, spodziewalem sie nazwy zmiennej..." << endl; return false; } cmd_info.s1 = cword; if(!get_token()) { cout << "Blad, spodziewalem sie znaku \'=\'" << endl; return false; } if(!get_param()) { cout << "Blad, spodziewalem sie wartosci poczatkowej licznika..." << endl; return false; } cmd_info.n1 = param; if(!get_token()) { cout << "Blad, spodziewalem sie slowa \'to\'" << endl; return false; } if(!get_param()) { cout << "Blad, spodziewalem sie wartosci koncowej licznika..." << endl; return false; } cmd_info.n2 = param; if(cmd_info.n1 > cmd_info.n2 || cmd_info.n1 < 0 || cmd_info.n2 < 0) { cout << "Bledne wartosci licznika petli! (" << cmd_info.n1 << " i " << cmd_info.n2 << ")" << endl; return false; } }

Teraz wykonanie. Nie jest to nic szczególnie trudnego; możemy posłużyć się analogią do poleceń if i call. Różnica będzie polegać na tym, że tutaj execute_commands będziemy wywoływać w pętli (oczywiście pętla ta obejmie również nasz mechanizm odwijania stosu). Poza tym wewnątrz wspomnianej pętli musimy oczywiście przed każdą iteracją zwiększać wartość zmiennej, pełniącej rolę licznika, zaś na zewnątrz, przed pętlą - wrzucić tę zmienną na stos, a po pętli - zdjąć ją z tego stosu.

Warto zauważyć, że instrukcja goto, wyskakująca poza obręb dowolnej naszej pętli spowoduje, że zmienna lokalna nie zostanie zdjęta ze stosu (co nie jest niczym specjalnie groźnym), natomiast wskoczenie do pętli z zewnątrz może spowodować brak licznika na stosie lub odwołanie do licznika innej, "nadrzędnej" pętli. Moglibyśmy zabezpieczyć się przed takimi sytuacjami, modyfikując odpowiednio zachowanie się naszej instrukcji goto, ale w ten sposób narobilibyśmy również zamieszania w artukule, więc rezygnujemy z tych środków ostrożności (uczulając niniejszym na problem).

case CMD_FOR: { int rbracket = -1; for(int j=i+1; j<cmd_array.size(); ++j) //szukanie prawej klamry { if(cmd_array[j].Type == CMD_RBRACKET && cmd_array[j].nNestingLevel == cmd_array[i].nNestingLevel) { rbracket = j; break; } } if(rbracket < 0) { bError = true; cout << "Blad wykonania skryptu - niedomkniety blok for" << endl; return true; } //wykonanie pętli ++nNestingLevel; VARIABLE counter; counter.name = cmd_array[i].s1; counter.value = cmd_array[i].n1; LocalVariables.push(counter); for(int k=cmd_array[i].n1; k<=cmd_array[i].n2; ++k) { execute_commands(cmd_array, i+1, rbracket); if(bError) return true; if(bUnwind) { if(nNestingLevel > 0) { --nNestingLevel; return false; } else { bUnwind = false; i = nNewPos; goto InsideLoop; } } // if(bUnwind) get_local_variable(cmd_array[i].s1, true); //zwiększ licznik pętli } // for(int k LocalVariables.pop(); i = rbracket; } break;

Wspomnieliśmy o zwiększaniu licznika pętli; robimy to po wykonaniu każdej iteracji. Licznik znajduje się na stosie, więc oczywiście nie możemy go modyfikować "z miejsca" - trzeba go odczytać ze stosu, zdjąć z niego, zwiększyć wartość i ponownie wrzucić na stos. Zajmuje się tym nowa funkcja get_local_variable, którą zaraz sobie dokładniej omówimy.

Poza wymienionymi rzeczami nie ma tu nic wartego omawiania, więc jedziemy dalej. Mamy przygotowaną zmienną lokalną na stosie, ale nigdzie nie wykorzystujemy jej wartości. Do tej pory jedyną możliwością wykorzystania zmiennej w naszym języku jest instrukcja if, ale ona na razie potrafi wykorzystać jedynie zmienne globalne (te deklarowane ze słowem var). Tak więc trzeba instrukcję if nieco zmodyfikować. Chcemy, aby w przypadku nieznalezienia właściwej zmiennej w wektorze Variables przeszukiwany był stos zmiennych lokalnych (czyli LocalVariables). Dzięki temu jeden i drugi rodzaj zmiennych będzie obsługiwany, przy czym zmienne globalne będą miały "pierwszeństwo".

W dotychczasowej postaci funkcji parse mieliśmy taką linijkę (w obsłudze polecenia if):

if(cmd_info.n1 < 0) return false;

Linijkę tę zamieniamy na następującą:

if(cmd_info.n1 < 0) cmd_info.s1 = cword; Tak więc nie przerywamy już parsingu, gdy napotkamy nieznaną zmienną; zamiast tego przyjmujemy, że jest to zmienna lokalna i zapamiętujemy jej nazwę. Dopiero jeśli w fazie wykonania nazwy tej nie znajdziemy na stosie zmiennych lokalnych, zgłaszamy błąd[1]. Drobnej modyfikacji będzie też wymagała funkcja get_var_index. Musimy w niej wykomentować jedną linijkę: int get_var_index(const string& sVarName) { for(int i=0; i<Variables.size(); ++i) if(Variables[i].name == sVarName) return i; // cout << "Blad, nie znaleziono zmiennej o nazwie \'" << sVarName << "\'." << endl; return -1; }

Pozostaje tylko poprawić funkcję is_true tak, aby pobierała wartość odpowiedniej zmiennej lokalnej, jeśli zmienna globalna nie została odnaleziona (tj. nVarIndex ma wartość -1). Problem w tym, że aby przeszukać stos, musimy z niego pozdejmować wszystkie "przeszkadzające" elementy (leżące nad tym, który aktualnie sprawdzamy), a przecież mogą się jeszcze kiedyś przydać :-). Tak więc musimy stworzyć drugi, tymczasowy stos, który pomoże nam w przeszukiwaniu tamtego. Działania te umieścimy w osobnej funkcji, o której już wspomnieliśmy. Do dzieła:

VARIABLE get_local_variable(const string& sName, bool bChangeValue) { stack<VARIABLE> temp; VARIABLE var; bool bFound = false; while(LocalVariables.size()) { var = LocalVariables.top(); LocalVariables.pop(); if(var.name == sName) { bFound = true; if(bChangeValue) ++var.value; } temp.push(var); if(bFound) break; } if(!bFound) { var.value = -1; } while(temp.size()) { LocalVariables.push(temp.top()); temp.pop(); } return var; }

Działanie tej funkcji jest następujące. Najpierw kolejno zdejmujemy wszystkie elementy ze stosu zmiennych lokalnych (jednocześnie wrzucając je na tymczasowy stos) i sprawdzamy każdy z nich, czy przypadkiem nie jest tą poszukiwaną zmienną. Jeśli tak, to przerywamy zdejmowanie i przechodzimy do drugiej pętli, która wkłada elementy z powrotem na stos zmiennych lokalnych. Jeśli wszystkie elementy zostały zdjęte i żaden nie jest tym właściwym, to nie wychodzimy z funkcji (przecież trzeba jeszcze przywrócić przeszukiwany stos do poprzedniej postaci), tylko ustawiamy zwracaną wartość na -1, co oznacza u nas błąd.

Dodatkowo funkcja get_local_variable potrafi ustawić dla znalezionej zmiennej nową wartość (konkretnie: zwiększyć ją o 1), co przydało nam się już wcześniej do zwiększania licznika pętli.

Stworzoną właśnie funkcję wykorzystamy ponownie w execute_commands, a dokładniej w miejscu, gdzie wywołujemy is_true. Nagłówek tej ostatniej musimy napierw trochę zmodyfikować - teraz nie będzie ona pobierać indeksu zmiennej, tylko jej wartość. Za tę wartość podstawiać - w zależności, czy będziemy mieć do czynienia ze zmienną globalną czy lokalną - wartość zmiennej z wektora Variables lub ze stosu LocalVariables.

bool is_true(int nVarValue, EOpType OpType, int nValue) { switch(OpType) { case OP_EQUAL: return (nVarValue == nValue); case OP_NOT_EQUAL: return (nVarValue != nValue); case OP_LESS: return (nVarValue < nValue); case OP_GREATER: return (nVarValue > nValue); case OP_LESS_OR_EQUAL: return (nVarValue <= nValue); case OP_GREATER_OR_EQUAL: return (nVarValue >= nValue); } return false; }

Teraz jeszcze tylko poprawimy wywołanie funkcji is_true (w miejscu, gdzie obsługujemy wykonanie polecenia CMD_IF) tak, aby zamiast indeksu zmiennej pobierała jej wartość. Linijkę:

if(is_true(cmd_array[i].n1, (EOpType)cmd_array[i].n2, cmd_array[i].n3))

...zamieniamy na:

int nVarValue; if(cmd_array[i].n1 < 0) { VARIABLE tmp; tmp = get_local_variable(cmd_array[i].s1, false); nVarValue = tmp.value; if(nVarValue < 0) { bError = true; cout << "Blad, nie zadeklarowano zmiennej " << cmd_array[i].s1 << endl; return true; } } else { nVarValue = Variables[cmd_array[i].n1].value; } if(is_true(nVarValue, (EOpType)cmd_array[i].n2, cmd_array[i].n3))

Mamy już wszystko, co potrzebne do funkcjonowania pętli. Możemy wykonać mały test:

for i = 2 to 5
{
 for j = 1 to 2
 {
  if i == 3
  {
   con_out "i rowne jest 3"
  }
 }
 con_out "Bla bla bla."
}

con_out "To byl test petli."

Dzięki naszym zabiegom ze stosem zmiennych lokalnych nawet w przypadku pętli zagnieżdżonych możliwe jest pobranie prawidłowego licznika pętli zewnętrznej w ciele pętli wewnętrznej. Obie pętle wykonują się dokładnie tyle razy, ile trzeba; liczniki nie są dostępne poza pętlami, a wartości początkowe i końcowe liczników nie mogą być nieprawidłowe, bo jest to sprawdzane jeszcze podczas parsingu. Tak więc wszystko gra :-).

Blok else

Jeden z największych dotychczasowych braków naszego języka to niemożność budowania bardziej złożonych konstrukcji z if-ów. Oczywiście, zawsze możemy tworzyć takie cuda:

if zmienna > 1
{
 con_out "Wieksza od 1"
}
if zmienna <= 1
{
 con_out "Nie większa od 1"
}

...ale taki sposób programowania oczywiście jest mało czytelny, co obniża jakość naszego języka. A wprowadzenie bloku else nie musi być wcale takie szalenie trudne. Żeby oszczędzić sobie pisania, zastosujemy małą sztuczkę: potraktujemy blok else jak blok if, tylko "odwrócimy" mu warunek i zmienimy kod na CMD_ELSE. Ten ostatni oczywiście najpierw deklarujemy:

enum ECmdType { CMD_UNKNOWN, CMD_ASSIGN_VALUE, CMD_RBRACKET, CMD_IF, CMD_ELSE, //nowe CMD_LABEL, CMD_GOTO, CMD_DEFPROC, CMD_CALLPROC, CMD_FOR, CMD_CON_OUT };

Podczas parsingu nie ma żadnych niespodzianek; po pobraniu słowa kluczowego przeskakujemy lewą klamrę i zwiększamy poziom zagnieżdżenia. I tyle. Oto jak to zrobimy:

else if(cword == "else") { cmd_info.Type = CMD_ELSE; if(!get_token() || cword != "{") { cout << "Blad, spodziewalem sie znaku \'{\'..." << endl; return false; } cmd_info.nNestingLevel = ++nCurNestingLevel; }

Nieco bardziej skomplikowane będzie zmodyfikowanie warstwy wykonania. W przypadku napotkania w tablicy polecenia CMD_ELSE przede wszystkim musimy znaleźć pasujące polecenie CMD_IF. Dlatego też przeszukujemy tablicę wstecz. Pasujący CMD_IF to oczywiście ten, który ma ten sam poziom zagnieżdżenia; chyba, że wredny użytkownik zrobi nam kawał i napisze taki skrypt:

var a 100
if a == 1
{
 con_out "Ten tekst nie bedzie wypisany"
}
con_out "Ten bedzie"
else
{
 con_out "Ten tez bedzie"
}
Moglibyśmy obronić się przed takim efektem, ale kosztowałoby nas to trochę wysiłku, dlatego zostawmy ten drobny błąd w spokoju - będzie zabawniej ;-). Ważniejszą sprawą jest to, co zrobimy w momencie znalezienia pasującego if-a. Aby wykorzystać istniejący już kod polecenia CMD_IF, modyfikujemy go następująco: case CMD_IF: case CMD_ELSE: { int rbracket = -1, matching_if = -1; if(cmd_array[i].Type == CMD_ELSE) { for(int j=i; j>0; --j) { if(cmd_array[j].Type == CMD_IF && cmd_array[j].nNestingLevel == cmd_array[i].nNestingLevel) { matching_if = j; break; } } //for(int j if(matching_if < 0) { bError = true; cout << "Blad wykonania skryptu - else bez pasujacego if" << endl; return true; } cmd_array[i] = negate_operator((EOpType)cmd_array[matching_if].n2); //"odwrócenie" warunku } //if(cmd_array[i].Type //dalej wszystko zostaje jak było

Jak widać, obecnie ten case "wyłapuje" dwa przypadki (CMD_IF i CMD_ELSE), poza tym rozbudowaliśmy go o fragment specyficzny dla przypadku CMD_ELSE. Fragment ten, jak już wspomnieliśmy, szuka pasującego bloku CMD_IF, "pożycza" sobie jego warunek i neguje go. Funkcja negująca operator będzie wyglądała tak:

EOpType negate_operator(EOpType op) { switch(op) { case OP_EQUAL: return OP_NOT_EQUAL; case OP_NOT_EQUAL: return OP_EQUAL; case OP_LESS: return OP_GREATER_OR_EQUAL; case OP_GREATER: return OP_LESS_OR_EQUAL; case OP_LESS_OR_EQUAL: return OP_GREATER; case OP_GREATER_OR_EQUAL: return OP_LESS; } return OP_UNKNOWN; }

Nie ma tu wiele do tłumaczenia. Po zanegowaniu warunku, blok else wykonywany jest dokładnie tak samo, jak if. Żeby było możliwe skompilowanie programu po tych zmianach, musimy oczywiście usunąć słówko const z nagłówka funkcji:

bool execute_commands(vector<COMMAND_INFO>& cmd_array, int start, int stop)

Pozostaje tylko sprawdzić, czy wszystko wyszło jak wyjść powinno:

var zm 1
if zm == 2
{
 con_out "Ten tekst sie nie wyswietli"
}
else
{
 con_out "Blok else"
}

Piszemy grę :-)

Teraz pobawimy się trochę - napiszemy sobie grę w naszym języku. Oczywiście nie ma co liczyć na cuda w rodzaju nowego Quake'a, ale przy użyciu samej konsoli też możemy sobie stworzyć grę. Będzie to kultowe "Kółko i krzyżyk". Musimy sobie jeszcze dorzucić kilka nowych poleceń:

enum ECmdType { CMD_UNKNOWN, CMD_ASSIGN_VALUE, CMD_RBRACKET, CMD_IF, CMD_ELSE, CMD_LABEL, CMD_GOTO, CMD_DEFPROC, CMD_CALLPROC, CMD_FOR, CMD_CON_OUT, CMD_CON_OUT_NOBR, //nowe CMD_CON_IN, //nowe CMD_CON_CLS //nowe CMD_SYSTEM //nowe };

Uspokajam, że to już ostatnia modyfikacja tego enum-a :-). Pierwsze z poleceń będzie wyświetlało tekst, ale bez przechodzenia do nowej linii. Drugie będzie pobierało tekst z klawiatury. Trzecie wreszcie posłuży nam do czyszczenia ekranu. To wszystko, czego będziemy potrzebowali - czwarte i ostatnie z nowych poleceń dodajemy tylko jako ciekawostkę. Parsing całej czwórki to banał:

else if(cword == "con_out_nobr") { cmd_info.Type = CMD_CON_OUT_NOBR; if(!get_string()) { cout << "Spodziewalem sie lancucha znakow..." << endl; return false; } cmd_info.s1 = cword; } else if(cword == "con_in") { cmd_info.Type = CMD_CON_IN; if(!get_token()) { cout << "Blad, spodziewalem sie nazwy zmiennej..." << endl; return false; } cmd_info.n1 = get_var_index(cword); } else if(cword == "con_cls") { cmd_info.Type = CMD_CON_CLS; } else if(cword == "system") { cmd_info.Type = CMD_SYSTEM; if(!get_string()) { cout << "Spodziewalem sie polecenia systemowego..." << endl; return false; } cmd_info.s1 = cword; }

Wykonanie jest nawet jeszcze prostsze:

case CMD_CON_OUT_NOBR: { cout << cmd_array[i].s1; } break; case CMD_CON_IN: { cin >> Variables[cmd_array[i].n1].value; } break; case CMD_CON_CLS: { system("cls"); } break; case CMD_SYSTEM: { system(cmd_array[i].s1.c_str()); } break;

Teraz możemy już napisać grę - tę i wiele, wiele innych :-).

// MinKiK - minimalistyczne kółko i krzyżyk :-)
// (c) 2006 Piotr Bednaruk

var z1 0
var z2 0
var z3 0
var z4 0
var z5 0
var z6 0
var z7 1
var z8 0
var z9 0

var ruch 0
var egejn 0

defproc Rysiek
{
 con_cls
 con_out "MinKiK, wersja 1.0"
 if z7 == 0 { con_out_nobr " " }
 if z7 == 1 { con_out_nobr "O" }
 if z7 == 2 { con_out_nobr "X" }
 con_out_nobr "|"
 if z8 == 0 { con_out_nobr " " }
 if z8 == 1 { con_out_nobr "O" }
 if z8 == 2 { con_out_nobr "X" }
 con_out_nobr "|"
 if z9 == 0 { con_out_nobr " " }
 if z9 == 1 { con_out_nobr "O" }
 if z9 == 2 { con_out_nobr "X" }
 con_out " "
 con_out "-----"
 if z4 == 0 { con_out_nobr " " }
 if z4 == 1 { con_out_nobr "O" }
 if z4 == 2 { con_out_nobr "X" }
 con_out_nobr "|"
 if z5 == 0 { con_out_nobr " " }
 if z5 == 1 { con_out_nobr "O" }
 if z5 == 2 { con_out_nobr "X" }
 con_out_nobr "|"
 if z6 == 0 { con_out_nobr " " }
 if z6 == 1 { con_out_nobr "O" }
 if z6 == 2 { con_out_nobr "X" }
 con_out " "
 con_out "-----"
 if z1 == 0 { con_out_nobr " " }
 if z1 == 1 { con_out_nobr "O" }
 if z1 == 2 { con_out_nobr "X" }
 con_out_nobr "|"
 if z2 == 0 { con_out_nobr " " }
 if z2 == 1 { con_out_nobr "O" }
 if z2 == 2 { con_out_nobr "X" }
 con_out_nobr "|"
 if z3 == 0 { con_out_nobr " " }
 if z3 == 1 { con_out_nobr "O" }
 if z3 == 2 { con_out_nobr "X" }
 con_out " "
}

defproc Egejn
{
 con_out_nobr "Czy chcesz zagrac jeszcze raz? (0==nie, inna liczba - tak) "
 con_in egejn
 if egejn == 0 { goto Koniec }
 z1 = 0 z2 = 0 z3 = 0 z4 = 0 z5 = 0 z6 = 0 z7 = 1 z8 = 0 z9 = 0
 goto Gra
}

Gra:
 call Rysiek

 //sprawdzenie wygranej kompa
 if z7 == 1 { if z8 == 1 { if z9 == 1 { goto WygranaKompa } } }
 if z4 == 1 { if z5 == 1 { if z6 == 1 { goto WygranaKompa } } }
 if z1 == 1 { if z2 == 1 { if z3 == 1 { goto WygranaKompa } } }
 if z7 == 1 { if z4 == 1 { if z1 == 1 { goto WygranaKompa } } }
 if z8 == 1 { if z5 == 1 { if z2 == 1 { goto WygranaKompa } } }
 if z9 == 1 { if z6 == 1 { if z3 == 1 { goto WygranaKompa } } }
 if z9 == 1 { if z5 == 1 { if z1 == 1 { goto WygranaKompa } } }
 if z7 == 1 { if z5 == 1 { if z3 == 1 { goto WygranaKompa } } }
 //remis?
 if z1 > 0 { if z2 > 0 { if z3 > 0 { if z4 > 0 { if z5 > 0 { if z6 > 0 { if z8 > 0 { if z9 > 0
 { goto Remis } } } } } } } }

 //ruch gracza
 con_out_nobr "Twoj ruch (patrz na klawiature numeryczna, 0==koniec): "
 con_in ruch
 if ruch == 1
 {
  if z1 > 0 { goto Gra }
  else { z1 = 2 }
 }
 if ruch == 2
 {
  if z2 > 0 { goto Gra }
  else { z2 = 2 }
 }
 if ruch == 3
 {
  if z3 > 0 { goto Gra }
  else { z3 = 2 }
 }
 if ruch == 4
 {
  if z4 > 0 { goto Gra }
  else { z4 = 2 }
 }
 if ruch == 5
 {
  if z5 > 0 { goto Gra }
  else { z5 = 2 }
 }
 if ruch == 6
 {
  if z6 > 0 { goto Gra }
  else { z6 = 2 }
 }
 //komp zawsze zaczyna z tego pola, więc jest ono ciągle zajęte
 if ruch == 7
 {
  goto Gra
 }
 if ruch == 8
 {
  if z8 > 0 { goto Gra }
  else { z8 = 2 }
 }
 if ruch == 9
 {
  if z9 > 0 { goto Gra }
  else { z9 = 2 }
 }
 if ruch == 0 { goto Koniec }
 if ruch > 9 { goto Gra }
 call Rysiek

 //sprawdzenie wygranej gracza
 if z4 == 2 { if z5 == 2 { if z6 == 2 { goto WygranaGracza } } }
 if z1 == 2 { if z2 == 2 { if z3 == 2 { goto WygranaGracza } } }
 if z8 == 2 { if z5 == 2 { if z2 == 2 { goto WygranaGracza } } }
 if z9 == 2 { if z6 == 2 { if z3 == 2 { goto WygranaGracza } } }
 if z9 == 2 { if z5 == 2 { if z1 == 2 { goto WygranaGracza } } }

 //ruch kompa
 if z5 == 0 { z5 = 1 goto Gra }
 if z1 == 0 { z1 = 1 goto Gra }
 if z9 == 0 { z9 = 1 goto Gra }
 if z8 == 0 { z8 = 1 goto Gra }
 if z4 == 0 { z4 = 1 goto Gra }
 if z3 == 0 { z3 = 1 goto Gra }
 if z2 == 0 { z2 = 1 goto Gra }
 if z6 == 0 { z6 = 1 goto Gra }

WygranaKompa:

 con_out "Niestety, przegrales z glupim AI!"
 call Egejn

WygranaGracza:

 con_out "Wygrales, gratulacje!"
 call Egejn

Remis:

 con_out "Remis."
 call Egejn

Koniec:

Tak, oto cała gra w kółko i krzyżyk :-). Nie jest to na pewno arcydzieło sztuki koderskiej (a dokładniej to każdy szanujący się programista powinien dostać na taki widok apopleksji), ale działa - mamy grę, napisaną w naszym własnym języku skryptowym!

Dodam jeszcze, że pisanie kompletnych gier od A do Z w języku skryptowym jest raczej rzadkością; tutaj uczyniliśmy to dlatego, by pokazać możliwości naszego nowego języka. Zazwyczaj skrypty wykorzystuje się jednak raczej w charakterze pomocniczym - dokładniej zostało to opisane we Wstępie.

Co jeszcze chciałbyś napisać? Może menedżer plików? Proszę bardzo - nasze nowe polecenie umożliwia nam wykonanie dowolnego polecenia systemowego! Popatrz tylko:

con_out "Teraz wypiszemy wszystkie pliki z biezacego foldera."
system "dir"
con_out "Wyniki zachowane zostaly w plik.txt"
system "dir > plik.txt"

Podsumowanie

Wynikiem naszych starań jest kompletny język skryptowy. Oczywiście "kompletny" w sensie, że obecne są w nim wszystkie najważniejsze elementy, takie jak instrukcje warunkowe, pętle, procedury, instrukcja skoku. Wiele elementów można by jeszcze dodać (tablice, wskaźniki, odpowiedniki instrukcji switch, break, continue, return itp.), można by również ulepszyć już istniejące (np. obsługa złożonych wyrażeń, argumenty dla funkcji...). Jednak główne zasady funkcjonowania języków skryptowych zostały pokazane.

Nasz język potrafi zrobić prawie wszystko, co może się odbywać w konsoli tekstowej. Jednak aby stał się "prawdziwym" językiem programowania, należałoby rozbudować jego zestaw poleceń tak, aby język nasz potrafił wykonywać typowe czynności, które wykonują zwykle standardowe biblioteki innych języków wysokopoziomowych: generować liczby losowe, konwertować liczby na tekst i odwrotnie, obliczać wartości funkcji matematycznych (sinus, pierwiastek, logarytm...), obsługiwać inne niż standardowe urządzenia wejściowe i wyjściowe (np. mysz), alokować pamięć etc. Fajną sprawą byłaby też możliwość wywoływania funkcji z DLL - wtedy moglibyśmy nawet tworzyć programy okienkowe w naszym języku :-). Wszystko to jednak wykracza poza wymagania, stawiane zazwyczaj językom skryptowym. Najczęściej bowiem język skryptowy wykonuje tylko czynności związane np. z grą, dla której został stworzony. Przykładem mogą być polecenia typu create_monster z pierwszej części tego artykułu.

Wielkim brakiem w naszym języku jest obsługa rozmaitych błędów, które może popełnić użytkownik przy wpisywaniu skryptu. Wyłapanie absolutnie wszystkich takich błędów jest oczywiście niemożliwe w praktyce; nawet twórcy kompilatorów tak popularnych języków, jak np. C++ nie ustrzegli się pewnych niedopatrzeń. Granica dopuszczalnej liczby takich błędów w języku skryptowym jest trudna do wyznaczenia; w tym artykule nie przejmowałem się nią zbytnio. Przedstawiony, przykładowy interpreter "przymyka oko" na bardzo wiele rzeczy; wszystko to jest raczej celowe. Im więcej błędów chcielibyśmy obsłużyć, tym mniej przejrzysty byłby przykładowy kod źródłowy interpretera, a artykuł - trudniejszy w odbiorze. Dlatego ograniczyłem obsługę błędów do niezbędnego minimum.

Z powodów wyżej wymienionych przykładowy język nie może pretendować do miana uniwersalnego języka skryptowego do jakiegokolwiek profesjonalnego zastosowania. Jednak myślę, że po drobnych przeróbkach można go z powodzeniem wykorzystać w nawet dość złożonej grze.

Mam nadzieję, że ten artykuł okaże się przydatny. Nawet jeśli pogardzisz moim przykładowym językiem skryptowym jako nieprzydatnym do "prawdziwych" zastosowań, albo w ogóle przedstawionymi metodami tworzenia takich języków, to przynajmniej może dzięki temu artykułowi będziesz miał okazję zajrzeć "do środka" języka i poznasz pewne uniwersalne reguły, którymi rządzi się jego tworzenie. Trzeba przyznać, że język programowania to jeden z tych wynalazków, których używamy na codzień, zwykle nie zastanawiając się głębiej, jak one funkcjonują. Może dzięki temu artykułowi będziesz trochę mniej narzekał, że funkcje w C++ muszą mieć swoje prototypy, nie da się deklarować wskaźników na funkcje składowe klasy, a kompilator pokazuje komunikaty kompletnie nieadekwatne do popełnionego błędu :-).

Link do kompletnego kodu źródłowego interpretera przedstawionego języka skryptowego:

skrypty-src.rar

------------------
[1] Oczywiście takie postępowanie nie jest do końca słuszne - wszystkie błędy, jakie da się wykryć w warstwie parsingu, powinny być zgłoszone już wtedy. Dzięki temu możemy oddzielić poszczególne warstwy (aby np. zrobić inną warstwę interpretacji/wykonania w specjalnym edytorze skryptów, a inną w samej grze). Myślę jednak, że w tym artykule możemy sobie pozwolić na nieco luźniejsze podejście.

[[Kategoria:c++]]

Tekst dodał:
Złośliwiec
27.08.2006 10:39

Ostatnia edycja:
Złośliwiec
27.08.2006 10:39

Kategorie:

Aby edytować tekst, musisz się zalogować.

# Edytuj Porównaj Czas Autor Rozmiar
#1 edytuj 27.08.2006 10:39 Złośliwiec 38.67 KB
Zwykły
Do sprawdzenia
Do akceptacji
  • starjacker0 (@starjacker0) 14 kwietnia 2014 08:24
    Skąd można pobrać dołączony kod źródłowy - skrypty-src.rar? Link już nie działa.
  • Napisz komentarz:
    Aby dodać swój komentarz, musisz się zalogować.
Licencja Creative Commons

Warsztat używa plików cookies. | Copyright © 2006-2017 Warsztat · Kontakt · Regulamin i polityka prywatności
build #ff080b4740 (Tue Mar 25 11:39:28 CET 2014)